<?php

namespace Cephalopod;

use Exception;

class Http
{
    const HTTP_EOL = "\r\n";
    const HTTP_FS = ': ';

    private static ?HttpTube $httpTube = null;

    public static function separate(string $originHttpMessage): array
    {
        $httpMessage = $originHttpMessage;
        if (strlen($httpMessage) <= 0) {
            throw new Exception('HTTP requests or responses should not be empty');
        }
        $firstLine = '';               // The first line of the HTTP message
        $headers   = [];               // Request or response header
        $body      = '';               // HTTP body
        $eol       = self::HTTP_EOL;   // \r\n
        $eolLength = strlen($eol);     // 2 Byte

        for ($i = 0; $i < 2; $i++) {
            $httpMessageLength = strlen($httpMessage);
            $contentLength = 0;

            $firstEolOffset = strpos($httpMessage, $eol);
            if (false === $firstEolOffset) {
                return ['', [], '', $httpMessage, false];
            }
            $firstLine = substr($httpMessage, 0, $firstEolOffset);
            if (
                0 !== strpos($firstLine, 'HTTP/')
                && 0 !== strpos($firstLine, 'GET')
                && 0 !== strpos($firstLine, 'HEAD')
                && 0 !== strpos($firstLine, 'POST')
                && 0 !== strpos($firstLine, 'PUT')
                && 0 !== strpos($firstLine, 'DELETE')
                && 0 !== strpos($firstLine, 'CONNECT')
                && 0 !== strpos($firstLine, 'OPTIONS')
                && 0 !== strpos($firstLine, 'TRACE')
                && 0 !== strpos($firstLine, 'PATCH')
                && 0 !== strpos($firstLine, 'MOVE')
                && 0 !== strpos($firstLine, 'COPY')
                && 0 !== strpos($firstLine, 'LINK')
                && 0 !== strpos($firstLine, 'UNLINK')
                && 0 !== strpos($firstLine, 'WRAPPED')
                && 0 !== strpos($firstLine, 'Extension-mothed')
            ) {
                if (0 == $i) {
                    $realDataBegin = strpos($httpMessage, 'HTTP/1.1 200 OK');
                    if (false !== $realDataBegin) {
                        $httpMessage = substr($httpMessage, $realDataBegin);
                        continue;
                    }
                }
                return [$firstLine, [], '', $httpMessage, false];
            }
        }

        $emptyLineOffset = strpos($httpMessage, $eol . $eol);
        if (false === $emptyLineOffset) {
            return [$firstLine, [], '', $httpMessage, false];
        }

        $headersBegin = $firstEolOffset + $eolLength;
        $headersLength = $emptyLineOffset - $headersBegin;

        $headers = [];
        if ($headersLength > 0) {
            foreach (explode($eol, substr($httpMessage, $headersBegin, $headersLength)) as $info) {
                $fieldValueInfo = static::separateFieldAndValue($info);
                if ('content-length' == strtolower($fieldValueInfo[0])) {
                    $contentLength = intval($fieldValueInfo[1]);
                }
                $headers[] = $fieldValueInfo;
            }
        }

        $bodyBegin = $emptyLineOffset + 2 * $eolLength;
        for ($i = 0; $i < 2; $i++) {
            if ($bodyBegin >= $httpMessageLength) {
                $body = '';
            } else {
                $body = substr($httpMessage, $bodyBegin, $contentLength);
            }
            if ($bodyBegin + $contentLength >= $httpMessageLength) {
                $unknowData = '';
            } else {
                $unknowData = substr($httpMessage, $bodyBegin + $contentLength);
            }
            $unknowDataSize = strlen($unknowData);
            if ($unknowDataSize > 0) {
                $firstChar = substr($unknowData, 0, 1);
                if (!in_array($firstChar, ['H', 'G', 'P', 'D', 'C', 'O', 'T', 'M', 'L', 'U', 'W', 'E'])) {
                    $offsetRealBodyEnd = strpos($unknowData, 'HTTP/1.1 200 OK');
                    if (false === $offsetRealBodyEnd) {
                        throw new Exception('Content-Length error, data is:' . PHP_EOL . static::readAbleBinrary($httpMessage));
                    }
                    $contentLength += $offsetRealBodyEnd;
                }
            } else {
                break;
            }
        }
        return [$firstLine, $headers, $body, $unknowData, strlen($body) == $contentLength];
    }

    public static function buildHttpMessage(string $firstLine, array $headers, string $body): string
    {
        $result = $firstLine . self::HTTP_EOL;
        foreach ($headers as $info) {
            if (!is_array($info) || !isset($info[0]) || !isset($info[1])) {
                throw new Exception('Unable to build HTTP message with given parameters');
            }
            $result .= ($info[0] . self::HTTP_FS . $info[1] . self::HTTP_EOL);
        }
        return $result . self::HTTP_EOL . $body;
    }

    public static function separateFieldAndValue(string $httpHeader): array
    {
        $httpFS = self::HTTP_FS;
        $fsOffset = strpos($httpHeader, $httpFS);
        if (false === $fsOffset) {
            throw new Exception('HTTP header format error, data begin:' . PHP_EOL . static::readAbleBinrary($httpHeader) . PHP_EOL . 'data end');
        }
        return [trim(substr($httpHeader, 0, $fsOffset)), trim(substr($httpHeader, $fsOffset + strlen($httpFS)))];
    }

    public static function getLocation(string $url): string
    {
        $s1 = strpos($url, '//');
        $s1 = false === $s1 ? 0 : $s1 + 2;
        $s2 = strpos($url, '/', $s1);
        if (false === $s2 || 0 === $s2) {
            return $url;
        }
        return substr($url, $s2);
    }

    public static function parse(string $text): array
    {
        $result = [
            'method' => '',
            'host' => '',
            'port' => '',
        ];
        try {
            list($firstLine) = static::separate($text);
        } catch (Exception $e) {
            return $result;
        }
        $keyInfo = explode(' ', $firstLine);
        if (empty($keyInfo) || 3 != count($keyInfo)) {
            return $result;
        }
        $method = $keyInfo[0];
        if ('CONNECT' == $method) {
            $result['method'] = $keyInfo[0];
            list($host, $port) = explode(':', $keyInfo[1]);
            if (empty($host) || empty($port)) {
                $result['method'] = '';
                return $result;
            }
            $result['host'] = $host;
            $result['port'] = intval($port);
            return $result;
        } else {
            $http = $keyInfo[1];
            $urlInfo = parse_url($http);
            if (empty($urlInfo['host'])) {
                return $result;
            }
            $result['host'] = $urlInfo['host'];
            $result['port'] = intval($urlInfo['port'] ?? 80);
            $result['method'] = $keyInfo[0];
            return $result;
        }
        return $result;
    }

    public static function removeProxyConnection(string $normalHttpMessage): string
    {
        list($firstLine, $headers, $body) = static::separate($normalHttpMessage);
        return static::removeProxy($firstLine, $headers, $body);
    }

    public static function removeProxy(string $firstLine, array $headers, string $body): string
    {
        $keyInfo = explode(' ', $firstLine);
        $keyInfo[1] = static::getLocation($keyInfo[1]);
        $firstLine = implode(' ', $keyInfo);

        $headerText = '';
        foreach ($headers as $index => &$header) {
            if ('proxy-connection' == strtolower($header[0])) {
                $header[0] = 'Connection';
            }
            $headerText .= implode(self::HTTP_FS, $header);
            if (isset($headers[$index + 1])) {
                $headerText .= self::HTTP_EOL;
            }
        }

        return $firstLine . self::HTTP_EOL . $headerText . self::HTTP_EOL . self::HTTP_EOL . $body;
    }

    public static function idealUnpacking(string &$inputData): array
    {
        $eol = self::HTTP_EOL;
        $endOfFirstLinePosition = strpos($inputData, $eol);
        if (false === $endOfFirstLinePosition) {
            return [
                'content_length' => -1,
                'unknow_data' => $inputData,
                'body' => '',
                'more' => -1,
            ];
        }
        $endOfHeaderPosition = strpos($inputData, $eol . $eol);
        if (false === $endOfHeaderPosition) {
            return [
                'content_length' => -1,
                'unknow_data' => $inputData,
                'body' => '',
                'more' => -1,
            ];
        }
        $contentLength = 0;
        $length = strlen($eol);
        $beginOfHeader = $endOfFirstLinePosition + $length;
        $lengthOfHeader = $endOfHeaderPosition - $beginOfHeader;

        $isMultipartFormData = false;
        $boundary = '';
        $headers = [];
        foreach (explode($eol, substr($inputData, $beginOfHeader, $lengthOfHeader)) as $raw) {
            $header = static::separateFieldAndValue($raw);
            $headers[] = $header;
            list($k, $v) = $header;
            if ('content-length' === strtolower($k)) {
                $contentLength = intval($v);
            }
            if ('content-type' == strtolower($k)) {
                if (0 === strpos(strtolower($v), 'multipart/form-data;')) {
                    $isMultipartFormData = true;
                    if (false !== ($offset = strpos($v, 'boundary='))) {
                        $boundary = substr($v, $offset + strlen('boundary='));
                    }
                }
            }
        }
        if ($contentLength < 0) {
            throw new Exception('Error, unknow data:' . static::readAbleBinrary($inputData));
        }
        $dataLength = strlen($inputData);
        $beginOfBody = $endOfHeaderPosition + 2 * $length; // header and first line total length
        $packageLengthMaybe = $contentLength + $beginOfBody;
        if ($isMultipartFormData) {
            if ($dataLength < $packageLengthMaybe) {
                return [
                    'content_length' => -1,
                    'unknow_data' => $inputData,
                    'body' => '',
                    'more' => -1,
                ];
            }
            if (is_null(static::$httpTube)) {
                static::$httpTube = new HttpTube();
            }
            return [
                'content_length' => $contentLength,
                'unknow_data' => $packageLengthMaybe > $dataLength ? '' : ($dataLength === $packageLengthMaybe ? '' : substr($inputData, $packageLengthMaybe)),
                'body' => static::$httpTube->getInsideData(substr($inputData, $beginOfBody), $headers),
                'more' => 0,
            ];
        }
        return [
            'content_length' => $contentLength,
            'unknow_data' => $packageLengthMaybe > $dataLength ? '' : ($dataLength === $packageLengthMaybe ? '' : substr($inputData, $packageLengthMaybe)),
            'body' => $dataLength < $packageLengthMaybe ? ($beginOfBody === $dataLength ? '' : substr($inputData, $beginOfBody)) : substr($inputData, $beginOfBody, $contentLength),
            'more' => $packageLengthMaybe > $dataLength ? $packageLengthMaybe - $dataLength : 0,
        ];
    }

    private static function readAbleBinrary(string $data): string
    {
        $baseChars = "0123456789abcdefghijklnmopqrstuvwxyzABCDEFGHIJKLNMOPQRSTUVWXYZ\n :-/\\?&";
        $len = strlen($data);
        $result = '';
        for ($i = 0; $i < $len; $i++) {
            $ch = substr($data, $i, 1);
            if (false === strpos($baseChars, $ch)) {
                $result .= '.';
            } elseif ("\n" == $ch) {
                $result .= PHP_EOL;
            } else {
                $result .= $ch;
            }
        }
        return $result;
    }
}
